﻿using System;
using System.Collections.Generic;
using System.DirectoryServices;
using System.IO;
using System.Runtime.CompilerServices;
using NEasy.Ldap.Entity;
using NEasy.Ldap.Extension;

namespace NEasy.Ldap
{
    /// <summary>
    /// class to access ldap principal data
    /// </summary>
    public class LdapRepository : ILdapRepository
    {
        private const string DirectoryEntryUrl = "LDAP://{0}/{1}";
        private DirectoryEntry _directoryEntry;

        /// <summary>
        /// ldap domain information for directory access
        /// </summary>
        protected LdapDomainInfo DomainInfo { get; set; }

        private DirectoryEntry DirectoryEntry
        {
            get
            {
                if (_directoryEntry == null) InitDirectoryEntry();
                return _directoryEntry;
            }
        }

        /// <summary>
        /// thread-save domain init method
        /// </summary>
        [MethodImpl(MethodImplOptions.Synchronized)]
        private void InitDirectoryEntry()
        {
            // validate state
            if (_directoryEntry != null) return;
            if (DomainInfo == null) throw new NEasyLdapException("DomainInfo for the Repository not initialized.");

            var url = string.Format(DirectoryEntryUrl, DomainInfo.Domain, DomainInfo.OU);

            // ReSharper disable ConvertIfStatementToConditionalTernaryExpression
            if (string.IsNullOrEmpty(DomainInfo.Username))
                _directoryEntry = new DirectoryEntry(url);
            else
                _directoryEntry = new DirectoryEntry(url, DomainInfo.Username, DomainInfo.Password);
        }

        #region Constructor

        /// <summary>
        /// creates a new directory entry
        /// </summary>
        public LdapRepository()
        {
        }

        /// <summary>
        /// creates a new directory entry
        /// </summary>
        public LdapRepository(LdapDomainInfo domainInfo)
        {
            DomainInfo = domainInfo;
        }

        #endregion

        #region Find

        /// <summary>
        /// Find a principal with his username
        /// </summary>
        /// <param name="primaryKey">primary key/value for filter</param>
        /// <returns>null if no entity was found</returns>
        /// <exception cref="ArgumentException">Throws if primaryKey definition is invalid</exception>
        /// <exception cref="InvalidDataException">Throws if search result contains more than one result</exception>
        private SearchResult Find(KeyValuePair<string, object> primaryKey)
        {
            // validate parameter
            if (string.IsNullOrEmpty(primaryKey.Key)) throw new ArgumentException("primary key on ldap object cannot be null");
            if (primaryKey.Value == null) throw new ArgumentException("value on ldap object cannot be null");

            // filter ldap
            var filter = string.Format("(({0}={1}))", primaryKey.Key, primaryKey.Value);
            var search = new DirectorySearcher(DirectoryEntry, filter);
            var searchResults = search.FindAll();
            if (searchResults.Count <= 0) return null;
            if (searchResults.Count > 1) throw new InvalidDataException("search contains more or less than one row: " + searchResults.Count);
            var searchResult = searchResults[0];

            // return the search result
            return searchResult;
        }

        /// <summary>
        /// Find a principal with his username
        /// </summary>
        public T Find<T>(T filterObject) where T : LdapObjectBase, new()
        {
            if (filterObject == null) throw new ArgumentException("filter cannot be null");

            // create dynamic filter and filter AD
            var keyValue = filterObject.GetPrimaryKeyValue();
            var searchResult = this.Find(keyValue);
            if (searchResult == null) return null;

            // convert entry to data principal instance
            var result = Activator.CreateInstance<T>();
            result.SetRepository(this);
            result.FillWithLdapAttributes(searchResult);

            return result;
        }

        #endregion

        #region Update

        /// <summary>
        /// Update entity in AD
        /// </summary>
        /// <param name="entity">entity</param>
        public bool Update<T>(T entity) where T : LdapObjectBase
        {
            if (entity == null) throw new NullReferenceException("entity cannot be null");

            // get AD entity, update values and save to AD
            var adEntity = this.Find(entity.GetPrimaryKeyValue());
            var adDirectoryEntity = adEntity.GetDirectoryEntry();

            entity.UpdateValuesTo(adDirectoryEntity);
            adDirectoryEntity.CommitChanges();

            return true;
        }

        #endregion

        #region SaveNew

        /// <summary>
        /// saves a new ad account into AD
        /// </summary>
        /// <param name="entity">entity</param>
        public bool SaveNew<T>(T entity) where T : LdapObjectBase
        {
            if (entity == null) throw new NullReferenceException("entity cannot be null");

            try
            {
                var adEntity = DirectoryEntry.Children.Add("CN=" + entity.CanonicalName, entity.SchemaClassName);
                entity.SaveValuesTo(adEntity);
                entity.SetRepository(this);
                adEntity.CommitChanges();

                return true;
            }
            catch (Exception ex)
            {
                throw new NEasyLdapException("Cannot save entity", ex);
            }
        }

        /// <summary>
        /// set the password of an entity
        /// </summary>
        /// <param name="entity">entity</param>
        /// <param name="password">password</param>
        public bool SetPassword<T>(T entity, string password) where T : LdapObjectBase
        {
            if (entity == null) throw new NullReferenceException("entity cannot be null");

            // get AD entity and invoke "set pasword" method
            var adEntity = this.Find(entity.GetPrimaryKeyValue());

            var adDirectoryEntity = adEntity.GetDirectoryEntry();
            adDirectoryEntity.Invoke("SetPassword", new object[] { password });
            adDirectoryEntity.CommitChanges();

            return true;
        }

        #endregion

        #region Filter

        /// <summary>
        /// filter the repository based on the ldap query
        /// </summary>
        /// <typeparam name="T">entity to retrieve</typeparam>
        /// <param name="filterQuery">ldap query</param>
        public IEnumerable<T> Filter<T>(string filterQuery) where T : LdapObjectBase, new()
        {
            var search = new DirectorySearcher(DirectoryEntry, filterQuery);
            var searchResults = search.FindAll();

            var result = new List<T>();
            foreach (SearchResult aResult in searchResults)
            {
                var element = (T)Activator.CreateInstance(typeof(T));
                element.SetRepository(this);
                element.FillWithLdapAttributes(aResult);
                result.Add(element);
            }
            return result;
        }

        #endregion
    }
}
